﻿using Swifter.Api.Types;
using Swifter.Data;
using Swifter.Tools;
using System;
using System.Collections.Generic;
using System.Text.RegularExpressions;

#nullable enable

namespace Swifter.Api
{
    public sealed partial class SqlCommandProcessor : ICommandProcessor
    {
        public static readonly Regex ConstantParametersRegex = new Regex($@"\$(?<{Group_Name_Name}>[A-Za-z_]+[A-Za-z0-9_]*)");
        public const string Group_Name_Name = "Name";
        public const string Argument_Name_Sql = "Sql";

        public readonly Database Database;

        public SqlCommandProcessor(Database database)
        {
            Database = database;
        }

        public CommandExtendedInfo[] ExtendedInfos => new CommandExtendedInfo[]
        {
            new CommandExtendedInfo{ Name = Argument_Name_Sql, Type = typeof(LongText) }
        };

        public RuntimeCommandInfo Build(BaseApplication application, CommandInfo commandInfo)
        {
            var sql = commandInfo.Extended[Argument_Name_Sql].ReadString();

            if (string.IsNullOrWhiteSpace(sql))
            {
                throw new ArgumentNullException(Argument_Name_Sql);
            }

            var constant_parameters = new List<RuntimeCommandInfo.ParameterInfo>();
            var variable_parameters = new List<RuntimeCommandInfo.ParameterInfo>();
            var results = new List<RuntimeCommandInfo.ResultInfo>();
            var components = new List<IComponent>();

            foreach (var item in commandInfo.Parameters)
            {
                if (string.IsNullOrEmpty(item.Name))
                {
                    throw new ArgumentNullException(nameof(item.Name));
                }

                switch (item.Name[0])
                {
                    case '@':
                        item.Name = item.Name.Substring(1);
                        variable_parameters.Add(ToRuntimeParameter(item));
                        break;
                    case '$':
                        item.Name.Substring(1);
                        constant_parameters.Add(ToRuntimeParameter(item));
                        break;
                    default:
                        variable_parameters.Add(ToRuntimeParameter(item));
                        break;
                }
            }

            foreach (var item in commandInfo.Results)
            {
                results.Add(ToRuntimeResult(item));
            }

            if (constant_parameters.Count != 0)
            {
                // 名字长的排前面
                constant_parameters.Sort((x, y) => x.Name.Length - y.Name.Length);

                var start = 0;

                foreach (Match item in ConstantParametersRegex.Matches(sql))
                {
                    if (item.Index != start)
                    {
                        var length = item.Index - start;

                        components.Add(new CodeComponent(sql.Substring(start, length)));
                    }

                    var name = item.Groups[Group_Name_Name].Value;

                    if (TryGetValue(constant_parameters, item => item.Name == name, out var parameter))
                    {
                        components.Add(new ConstantComponent(parameter.Name));
                    }
                    else
                    {
                        components.Add(new CodeComponent(item.Value));
                    }

                    start = item.Index + item.Length;
                }

                if (start != sql.Length)
                {
                    components.Add(new CodeComponent(sql.Substring(start)));
                }
            }
            else
            {
                components.Add(new CodeComponent(sql));
            }

            foreach (var item in variable_parameters)
            {
                components.Add(new VariableComponent(item.Name));
            }

            var runtimeCommandInfo = new RuntimeCommandInfo(commandInfo.Name, commandInfo.Version, this)
            {
                CacheKey = commandInfo.CacheKey,
                LockKey = commandInfo.LockKey,
                NeedAuthorized = (commandInfo.Flags & CommandFlags.Authorization) != 0
            };

            runtimeCommandInfo.Invoker = new Invoker(
                Database,
                components.ToArray(),
                runtimeCommandInfo,
                commandInfo.Flags);

            foreach (var item in constant_parameters)
            {
                runtimeCommandInfo.parameters.Add(item.Name, item);
            }

            foreach (var item in variable_parameters)
            {
                runtimeCommandInfo.parameters.Add(item.Name, item);
            }

            foreach (var item in results)
            {
                runtimeCommandInfo.results.Add(item.Name, item);
            }

            return runtimeCommandInfo;

            RuntimeCommandInfo.ParameterInfo ToRuntimeParameter(CommandInfo.ParameterInfo parameterInfo)
            {
                var type = application.GetType(parameterInfo.Type);

                return new RuntimeCommandInfo.ParameterInfo {
                    Name = parameterInfo.Name,
                    Type = type,
                    DefaultValue = XConvert.ToObject(parameterInfo.DefaultValue, type)
                };
            }

            RuntimeCommandInfo.ResultInfo ToRuntimeResult(CommandInfo.ResultInfo resultInfo)
            {
                var type = application.GetType(resultInfo.Type);

                return new RuntimeCommandInfo.ResultInfo {
                    Name = resultInfo.Name,
                    Type = type
                };
            }

            static bool TryGetValue<T>(List<T> list, Predicate<T> match, out T outValue)
            {
                foreach (var item in list)
                {
                    if (match(item))
                    {
                        outValue = item;

                        return true;
                    }
                }

                outValue = default;

                return false;
            }
        }

        interface IComponent
        {
            string GetCode(ComponentParameters parameters);
        }

        sealed class CodeComponent : IComponent
        {
            public readonly string Code;

            public CodeComponent(string code)
            {
                Code = code;
            }

            public string GetCode(ComponentParameters parameters)
            {
                return Code;
            }
        }

        sealed class ConstantComponent : IComponent
        {
            public readonly string Name;

            public ConstantComponent(string name)
            {
                Name = name;
            }

            public string GetCode(ComponentParameters parameters)
            {
                var sqlBuilder = parameters.Invoker.Database.CreateSQLBuilder();

                parameters.ContextParameters.CommandParameters.TryGetValue(Name, out var value);

                sqlBuilder.BuildValue(SqlHelper.ValueOf(value.Value));

                return sqlBuilder.ToSql();
            }
        }

        sealed class VariableComponent : IComponent
        {
            public readonly string Name;

            public VariableComponent(string name)
            {
                Name = name;
            }

            public string GetCode(ComponentParameters parameters)
            {
                parameters.ContextParameters.CommandParameters.TryGetValue(Name, out var value);

                parameters.Parameters.Add(Name, value.Value);

                return string.Empty;
            }
        }

        sealed class ComponentParameters
        {
            public readonly Invoker Invoker;
            public readonly Dictionary<string, object> Parameters;
            public readonly ContextInfo ContextInfo;
            public readonly ContextParameters ContextParameters;

            public ComponentParameters(Invoker invoker, ContextInfo contextInfo, ContextParameters contextParameters)
            {
                Parameters = new Dictionary<string, object>();

                Invoker = invoker;
                ContextInfo = contextInfo;
                ContextParameters = contextParameters;
            }
        }

        sealed class Invoker : ICommandInvoker
        {
            public readonly Database Database;
            public readonly IComponent[] Components;
            public readonly RuntimeCommandInfo CommandInfo;

            public readonly bool IsSelect;

            public Invoker(Database database, IComponent[] components, RuntimeCommandInfo commandInfo, CommandFlags flags)
            {
                Database = database;
                Components = components;
                CommandInfo = commandInfo;

                if ((flags & CommandFlags.Select) != 0)
                {
                    IsSelect = true;
                }
            }

            public void Invoke(ContextInfo context, ContextParameters contextParameters, Action<object?> callback)
            {
                var parameters = new ComponentParameters(this, context, contextParameters);
                var codes = new string[Components.Length];

                for (int i = 0; i < codes.Length; i++)
                {
                    codes[i] = Components[i].GetCode(parameters);
                }

                var code = string.Concat(codes);

                if (IsSelect)
                {
                    Database.ExecuteReaderAsync(code, (exception, data_reader) =>
                    {
                        callback((object)exception ?? data_reader);

                    }, parameters.Parameters);
                }
                else
                {
                    Database.ExecuteNonQueryAsync(code, (exception, rows) =>
                    {
                        callback(new { Rows = rows });
                    }, parameters.Parameters);
                }
            }
        }
    }
}